package system

import (
	"bytes"
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"github.com/cdfmlr/ellipsis"
	"github.com/disintegration/imaging"
	"github.com/google/uuid"
	"image"
	"image/png"
	"os"
	"slices"
	"strconv"
	"strings"
	"time"
	"wox/plugin"
	"wox/setting/definition"
	"wox/util"
	"wox/util/clipboard"
	"wox/util/keyboard"
	"wox/util/window"
)

var clipboardIcon = plugin.NewWoxImageBase64(``)
var isKeepTextHistorySettingKey = "is_keep_text_history"
var textHistoryDaysSettingKey = "text_history_days"
var isKeepImageHistorySettingKey = "is_keep_image_history"
var imageHistoryDaysSettingKey = "image_history_days"
var primaryActionSettingKey = "primary_action"
var primaryActionValueCopy = "copy"
var primaryActionValuePaste = "paste"

func init() {
	plugin.AllSystemPlugin = append(plugin.AllSystemPlugin, &ClipboardPlugin{
		maxHistoryCount:   5000,
		historyImageCache: util.NewHashMap[string, clipboardImageCache](),
	})
}

type ClipboardHistory struct {
	Id         string
	Data       clipboard.Data
	Icon       plugin.WoxImage
	Timestamp  int64
	IsFavorite bool
}

type ClipboardHistoryJson struct {
	Id         string
	DataType   clipboard.Type
	Data       []byte
	Icon       plugin.WoxImage
	Timestamp  int64
	IsFavorite bool
}

func (c *ClipboardHistory) MarshalJSON() ([]byte, error) {
	var data = ClipboardHistoryJson{
		Id:         c.Id,
		Icon:       c.Icon,
		Timestamp:  c.Timestamp,
		IsFavorite: c.IsFavorite,
	}
	if c.Data != nil {
		marshalJSON, err := c.Data.MarshalJSON()
		if err != nil {
			return nil, err
		}
		data.DataType = c.Data.GetType()
		data.Data = marshalJSON
	}

	return json.Marshal(data)
}

func (c *ClipboardHistory) UnmarshalJSON(data []byte) error {
	var clipboardHistoryJson ClipboardHistoryJson
	err := json.Unmarshal(data, &clipboardHistoryJson)
	if err != nil {
		return err
	}

	c.Id = clipboardHistoryJson.Id
	c.Icon = clipboardHistoryJson.Icon
	c.Timestamp = clipboardHistoryJson.Timestamp
	c.IsFavorite = clipboardHistoryJson.IsFavorite

	if clipboardHistoryJson.Data != nil {
		var clipboardDataType = clipboardHistoryJson.DataType
		if clipboardDataType == clipboard.ClipboardTypeText {
			var textData = clipboard.TextData{}
			unmarshalErr := json.Unmarshal(clipboardHistoryJson.Data, &textData)
			if unmarshalErr != nil {
				return unmarshalErr
			}
			c.Data = &textData
		} else if clipboardDataType == clipboard.ClipboardTypeFile {
			var filePathData = clipboard.FilePathData{}
			unmarshalErr := json.Unmarshal(clipboardHistoryJson.Data, &filePathData)
			if unmarshalErr != nil {
				return unmarshalErr
			}
			c.Data = &filePathData
		} else if clipboardDataType == clipboard.ClipboardTypeImage {
			var imageData = clipboard.ImageData{}
			unmarshalErr := json.Unmarshal(clipboardHistoryJson.Data, &imageData)
			if unmarshalErr != nil {
				return unmarshalErr
			}
			c.Data = &imageData
		} else {
			return fmt.Errorf("unsupported clipboard data type: %s", clipboardDataType)
		}
	}

	return nil

}

type clipboardImageCache struct {
	preview plugin.WoxImage
	icon    plugin.WoxImage
}

type ClipboardPlugin struct {
	api               plugin.API
	history           []ClipboardHistory
	historyImageCache *util.HashMap[string, clipboardImageCache]
	maxHistoryCount   int
}

func (c *ClipboardPlugin) GetMetadata() plugin.Metadata {
	return plugin.Metadata{
		Id:            "5f815d98-27f5-488d-a756-c317ea39935b",
		Name:          "Clipboard History",
		Author:        "Wox Launcher",
		Website:       "https://github.com/Wox-launcher/Wox",
		Version:       "1.0.0",
		MinWoxVersion: "2.0.0",
		Runtime:       "Go",
		Description:   "Clipboard history for Wox",
		Icon:          clipboardIcon.String(),
		Entry:         "",
		TriggerKeywords: []string{
			"cb",
		},
		Features: []plugin.MetadataFeature{
			{
				Name: plugin.MetadataFeatureIgnoreAutoScore,
			},
		},
		Commands: []plugin.MetadataCommand{
			{
				Command:     "fav",
				Description: "List favorite clipboard history",
			},
		},
		SupportedOS: []string{
			"Windows",
			"Macos",
			"Linux",
		},
		SettingDefinitions: []definition.PluginSettingDefinitionItem{
			{
				Type: definition.PluginSettingDefinitionTypeCheckBox,
				Value: &definition.PluginSettingValueCheckBox{
					Key:          isKeepTextHistorySettingKey,
					Label:        "i18n:plugin_clipboard_keep_text_history",
					DefaultValue: "true",
				},
			},
			{
				Type: definition.PluginSettingDefinitionTypeTextBox,
				Value: &definition.PluginSettingValueTextBox{
					Key:          textHistoryDaysSettingKey,
					Suffix:       "i18n:plugin_clipboard_days",
					DefaultValue: "90",
				},
			},
			{
				Type: definition.PluginSettingDefinitionTypeNewLine,
			},
			{
				Type: definition.PluginSettingDefinitionTypeCheckBox,
				Value: &definition.PluginSettingValueCheckBox{
					Key:          isKeepImageHistorySettingKey,
					Label:        "i18n:plugin_clipboard_keep_image_history",
					DefaultValue: "true",
				},
			},
			{
				Type: definition.PluginSettingDefinitionTypeTextBox,
				Value: &definition.PluginSettingValueTextBox{
					Key:          imageHistoryDaysSettingKey,
					Suffix:       "i18n:plugin_clipboard_days",
					DefaultValue: "3",
				},
			},
			{
				Type: definition.PluginSettingDefinitionTypeNewLine,
			},
			{
				Type: definition.PluginSettingDefinitionTypeSelect,
				Value: &definition.PluginSettingValueSelect{
					Key:          primaryActionSettingKey,
					Label:        "i18n:plugin_clipboard_primary_action",
					DefaultValue: primaryActionValuePaste,
					Options: []definition.PluginSettingValueSelectOption{
						{Label: "i18n:plugin_clipboard_primary_action_copy_to_clipboard", Value: primaryActionValueCopy},
						{Label: "i18n:plugin_clipboard_primary_action_paste_to_active_app", Value: primaryActionValuePaste},
					},
				},
			},
		},
	}
}

func (c *ClipboardPlugin) Init(ctx context.Context, initParams plugin.InitParams) {
	c.api = initParams.API
	c.loadHistory(ctx)
	clipboard.Watch(func(data clipboard.Data) {
		c.api.Log(ctx, plugin.LogLevelInfo, fmt.Sprintf("clipboard data changed, type=%s", data.GetType()))
		// ignore file type
		if data.GetType() == clipboard.ClipboardTypeFile {
			return
		}

		if data.GetType() == clipboard.ClipboardTypeText && !c.isKeepTextHistory(ctx) {
			return
		}
		if data.GetType() == clipboard.ClipboardTypeImage && !c.isKeepImageHistory(ctx) {
			return
		}

		icon := c.getDefaultTextIcon()

		if data.GetType() == clipboard.ClipboardTypeText {
			textData := data.(*clipboard.TextData)
			if len(textData.Text) == 0 {
				return
			}
			if strings.TrimSpace(textData.Text) == "" {
				return
			}

			if iconImage, iconErr := c.getActiveWindowIcon(ctx); iconErr == nil {
				icon = iconImage
			}
		}

		// if last history is same with current changed one, ignore it
		if len(c.history) > 0 {
			lastHistory := c.history[len(c.history)-1]
			if lastHistory.Data.GetType() == data.GetType() {
				if data.GetType() == clipboard.ClipboardTypeText {
					changedTextData := data.(*clipboard.TextData)
					lastTextData := lastHistory.Data.(*clipboard.TextData)
					if lastTextData.Text == changedTextData.Text {
						c.history[len(c.history)-1].Timestamp = util.GetSystemTimestamp()
						return
					}
				}

				if data.GetType() == clipboard.ClipboardTypeImage {
					changedImageData := data.(*clipboard.ImageData)
					lastImageData := lastHistory.Data.(*clipboard.ImageData)
					// if image size is same, ignore it
					if lastImageData.Image.Bounds().Eq(changedImageData.Image.Bounds()) {
						c.history[len(c.history)-1].Timestamp = util.GetSystemTimestamp()
						return
					}
				}
			}
		}

		history := ClipboardHistory{
			Id:         uuid.NewString(),
			Data:       data,
			Timestamp:  util.GetSystemTimestamp(),
			Icon:       icon,
			IsFavorite: false,
		}

		if data.GetType() == clipboard.ClipboardTypeImage {
			c.generateHistoryImageCache(ctx, history)
		}

		c.history = append(c.history, history)
		c.saveHistory(ctx)
	})
}

func (c *ClipboardPlugin) Query(ctx context.Context, query plugin.Query) []plugin.QueryResult {
	var results []plugin.QueryResult

	if query.Command == "fav" {
		for i := len(c.history) - 1; i >= 0; i-- {
			history := c.history[i]
			if history.IsFavorite {
				results = append(results, c.convertClipboardData(ctx, history))
			}
		}
		return results
	}

	if query.Search == "" {
		//return top 50 clipboard history order by desc
		var count = 0
		for i := len(c.history) - 1; i >= 0; i-- {
			history := c.history[i]
			startTimestamp := util.GetSystemTimestamp()
			results = append(results, c.convertClipboardData(ctx, history))
			c.api.Log(ctx, plugin.LogLevelInfo, fmt.Sprintf("convert clipboard data cost %d ms", util.GetSystemTimestamp()-startTimestamp))
			count++

			if count >= 50 {
				break
			}
		}
		return results
	}

	//only text support search
	for i := len(c.history) - 1; i >= 0; i-- {
		history := c.history[i]
		if history.Data.GetType() == clipboard.ClipboardTypeText {
			historyData := history.Data.(*clipboard.TextData)
			if strings.Contains(strings.ToLower(historyData.Text), strings.ToLower(query.Search)) {
				results = append(results, c.convertClipboardData(ctx, history))
			}
		}
	}

	return results
}

func (c *ClipboardPlugin) convertClipboardData(ctx context.Context, history ClipboardHistory) plugin.QueryResult {
	if history.Data.GetType() == clipboard.ClipboardTypeText {
		historyData := history.Data.(*clipboard.TextData)

		if history.Icon.ImageType == plugin.WoxImageTypeAbsolutePath {
			// if image doesn't exist, use default icon
			if _, err := os.Stat(history.Icon.ImageData); err != nil {
				history.Icon = c.getDefaultTextIcon()
			}
		}

		primaryActionCode := c.api.GetSetting(ctx, primaryActionSettingKey)

		actions := []plugin.QueryResultAction{
			{
				Name:      "Copy to clipboard",
				IsDefault: primaryActionValueCopy == primaryActionCode,
				Action: func(ctx context.Context, actionContext plugin.ActionContext) {
					c.moveHistoryToTop(ctx, history.Id)
					clipboard.Write(history.Data)
				},
			},
			{
				Name:      "Paste to active app",
				IsDefault: primaryActionValuePaste == primaryActionCode,
				Action: func(ctx context.Context, actionContext plugin.ActionContext) {
					c.moveHistoryToTop(ctx, history.Id)
					clipboard.Write(history.Data)
					util.Go(context.Background(), "clipboard history copy", func() {
						time.Sleep(time.Millisecond * 100)
						err := keyboard.SimulatePaste()
						if err != nil {
							c.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("simulate paste clipboard failed, err=%s", err.Error()))
						} else {
							c.api.Log(ctx, plugin.LogLevelInfo, "simulate paste clipboard success")
						}
					})
				},
			},
		}
		if !history.IsFavorite {
			actions = append(actions, plugin.QueryResultAction{
				Name: "Save as favorite",
				Icon: plugin.NewWoxImageBase64(``),
				Action: func(ctx context.Context, actionContext plugin.ActionContext) {
					needSave := false
					for i := range c.history {
						if c.history[i].Id == history.Id {
							c.history[i].IsFavorite = true
							needSave = true
							break
						}
					}
					if needSave {
						c.api.Log(ctx, plugin.LogLevelInfo, fmt.Sprintf("save history as favorite, id=%s", history.Id))
						c.saveHistory(ctx)
					}
				},
			})
		} else {
			actions = append(actions, plugin.QueryResultAction{
				Name: "Cancel favorite",
				Icon: plugin.NewWoxImageBase64(``),
				Action: func(ctx context.Context, actionContext plugin.ActionContext) {
					needSave := false
					for i := range c.history {
						if c.history[i].Id == history.Id {
							c.history[i].IsFavorite = false
							needSave = true
							break
						}
					}
					if needSave {
						c.api.Log(ctx, plugin.LogLevelInfo, fmt.Sprintf("cancel history favorite, id=%s", history.Id))
						c.saveHistory(ctx)
					}
				},
			})
		}

		return plugin.QueryResult{
			Title: strings.TrimSpace(ellipsis.Centering(historyData.Text, 80)),
			Icon:  history.Icon,
			Preview: plugin.WoxPreview{
				PreviewType: plugin.WoxPreviewTypeText,
				PreviewData: historyData.Text,
				PreviewProperties: map[string]string{
					"i18n:plugin_clipboard_copy_date":       util.FormatTimestamp(history.Timestamp),
					"i18n:plugin_clipboard_copy_characters": fmt.Sprintf("%d", len(historyData.Text)),
				},
			},
			Score:   history.Timestamp,
			Actions: actions,
		}
	}

	if history.Data.GetType() == clipboard.ClipboardTypeImage {
		historyData := history.Data.(*clipboard.ImageData)

		var previewWoxImage, iconWoxImage plugin.WoxImage
		if v, ok := c.historyImageCache.Load(history.Id); ok {
			previewWoxImage = v.preview
			iconWoxImage = v.icon
		} else {
			previewWoxImage, iconWoxImage = c.generateHistoryImageCache(ctx, history)
		}

		return plugin.QueryResult{
			Title: fmt.Sprintf("Image (%d*%d) (%s)", historyData.Image.Bounds().Dx(), historyData.Image.Bounds().Dy(), c.getImageSize(ctx, historyData.Image)),
			Icon:  iconWoxImage,
			Preview: plugin.WoxPreview{
				PreviewType: plugin.WoxPreviewTypeImage,
				PreviewData: previewWoxImage.String(),
				PreviewProperties: map[string]string{
					"i18n:plugin_clipboard_copy_date":    util.FormatTimestamp(history.Timestamp),
					"i18n:plugin_clipboard_image_width":  fmt.Sprintf("%d", historyData.Image.Bounds().Dx()),
					"i18n:plugin_clipboard_image_height": fmt.Sprintf("%d", historyData.Image.Bounds().Dy()),
				},
			},
			Score: history.Timestamp,
			Actions: []plugin.QueryResultAction{
				{
					Name: "Copy to clipboard",
					Action: func(ctx context.Context, actionContext plugin.ActionContext) {
						clipboard.Write(history.Data)
					},
				},
			},
		}
	}

	return plugin.QueryResult{
		Title: "ERR: Unknown history data type",
	}
}

func (c *ClipboardPlugin) generateHistoryImageCache(ctx context.Context, history ClipboardHistory) (previewImg, iconImg plugin.WoxImage) {
	historyData := history.Data.(*clipboard.ImageData)

	compressedPreviewImg := imaging.Resize(historyData.Image, 400, 0, imaging.Lanczos)
	compressedIconImg := imaging.Resize(historyData.Image, 40, 0, imaging.Lanczos)
	previewImage, err := plugin.NewWoxImage(compressedPreviewImg)
	if err != nil {
		previewImage = c.getDefaultTextIcon()
	}
	iconImage, iconErr := plugin.NewWoxImage(compressedIconImg)
	if iconErr != nil {
		iconImage = plugin.NewWoxImageBase64(``)
	}

	c.historyImageCache.Store(history.Id, clipboardImageCache{
		preview: previewImage,
		icon:    iconImage,
	})

	c.api.Log(ctx, plugin.LogLevelInfo, fmt.Sprintf("generate history image preview and icon cache, id=%s", history.Id))
	return previewImage, iconImage
}

func (c *ClipboardPlugin) getImageSize(ctx context.Context, image image.Image) string {
	bounds := image.Bounds()
	sizeMb := float64(bounds.Dx()*bounds.Dy()) * 24 / 8 / 1024 / 1024
	return fmt.Sprintf("%.2f MB", sizeMb)
}

func (c *ClipboardPlugin) moveHistoryToTop(ctx context.Context, id string) {
	for i := range c.history {
		if c.history[i].Id == id {
			c.history[i].Timestamp = util.GetSystemTimestamp()
			break
		}
	}

	// sort history by timestamp asc
	slices.SortStableFunc(c.history, func(i, j ClipboardHistory) int {
		return int(i.Timestamp - j.Timestamp)
	})
}

func (c *ClipboardPlugin) getActiveWindowIcon(ctx context.Context) (plugin.WoxImage, error) {
	icon, err := window.GetActiveWindowIcon()
	if err != nil {
		return plugin.WoxImage{}, err
	}

	var buf bytes.Buffer
	encodeErr := png.Encode(&buf, icon)
	if encodeErr != nil {
		return plugin.WoxImage{}, encodeErr
	}

	base64Str := base64.StdEncoding.EncodeToString(buf.Bytes())
	return plugin.NewWoxImageBase64(fmt.Sprintf("data:image/png;base64,%s", base64Str)), nil
}

func (c *ClipboardPlugin) saveHistory(ctx context.Context) {
	startTimestamp := util.GetSystemTimestamp()

	var favoriteHistories []ClipboardHistory
	var normalHistories []ClipboardHistory
	for i := len(c.history) - 1; i >= 0; i-- {
		if c.history[i].Data == nil {
			continue
		}
		if c.history[i].IsFavorite {
			favoriteHistories = append(favoriteHistories, c.history[i])
			continue
		}

		if c.history[i].Data.GetType() == clipboard.ClipboardTypeText {
			if util.GetSystemTimestamp()-c.history[i].Timestamp > int64(c.getTextHistoryDays(ctx))*24*60*60*1000 {
				continue
			}
		}
		if c.history[i].Data.GetType() == clipboard.ClipboardTypeImage {
			if util.GetSystemTimestamp()-c.history[i].Timestamp > int64(c.getImageHistoryDays(ctx))*24*60*60*1000 {
				continue
			}
		}

		normalHistories = append(normalHistories, c.history[i])
	}

	histories := append(favoriteHistories, normalHistories...)

	// sort history by timestamp asc
	slices.SortStableFunc(histories, func(i, j ClipboardHistory) int {
		return int(i.Timestamp - j.Timestamp)
	})

	historyJson, marshalErr := json.Marshal(histories)
	if marshalErr != nil {
		c.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("marshal clipboard text history failed, err=%s", marshalErr.Error()))
		return
	}

	c.api.SaveSetting(ctx, "history", string(historyJson), false)
	c.api.Log(ctx, plugin.LogLevelInfo, fmt.Sprintf("save clipboard history, count:%d, cost:%dms", len(c.history), util.GetSystemTimestamp()-startTimestamp))
}

func (c *ClipboardPlugin) loadHistory(ctx context.Context) {
	historyJson := c.api.GetSetting(ctx, "history")
	if historyJson == "" {
		return
	}

	startTimestamp := util.GetSystemTimestamp()
	var history []ClipboardHistory
	unmarshalErr := json.Unmarshal([]byte(historyJson), &history)
	if unmarshalErr != nil {
		c.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("unmarshal clipboard text history failed, err=%s", unmarshalErr.Error()))
	}

	//sort history by timestamp asc
	slices.SortStableFunc(history, func(i, j ClipboardHistory) int {
		return int(i.Timestamp - j.Timestamp)
	})

	c.api.Log(ctx, plugin.LogLevelInfo, fmt.Sprintf("load clipboard history, count=%d, cost=%dms", len(history), util.GetSystemTimestamp()-startTimestamp))
	c.history = history
}

func (c *ClipboardPlugin) getDefaultTextIcon() plugin.WoxImage {
	return plugin.NewWoxImageSvg(`<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="48" height="48" viewBox="0 0 48 48"><path fill="#90CAF9" d="M40 45L8 45 8 3 30 3 40 13z"></path><path fill="#E1F5FE" d="M38.5 14L29 14 29 4.5z"></path><path fill="#1976D2" d="M16 21H33V23H16zM16 25H29V27H16zM16 29H33V31H16zM16 33H29V35H16z"></path></svg>`)
}

func (c *ClipboardPlugin) isKeepTextHistory(ctx context.Context) bool {
	isKeepTextHistory := c.api.GetSetting(ctx, isKeepTextHistorySettingKey)
	if isKeepTextHistory == "" {
		return true
	}

	isKeepTextHistoryBool, err := strconv.ParseBool(isKeepTextHistory)
	if err != nil {
		return true
	}

	return isKeepTextHistoryBool
}

func (c *ClipboardPlugin) getTextHistoryDays(ctx context.Context) int {
	textHistoryDays := c.api.GetSetting(ctx, textHistoryDaysSettingKey)
	if textHistoryDays == "" {
		return 90
	}

	textHistoryDaysInt, err := strconv.Atoi(textHistoryDays)
	if err != nil {
		return 90
	}

	return textHistoryDaysInt
}

func (c *ClipboardPlugin) isKeepImageHistory(ctx context.Context) bool {
	isKeepImageHistory := c.api.GetSetting(ctx, isKeepImageHistorySettingKey)
	if isKeepImageHistory == "" {
		return true
	}

	isKeepImageHistoryBool, err := strconv.ParseBool(isKeepImageHistory)
	if err != nil {
		return true
	}

	return isKeepImageHistoryBool
}

func (c *ClipboardPlugin) getImageHistoryDays(ctx context.Context) int {
	imageHistoryDays := c.api.GetSetting(ctx, imageHistoryDaysSettingKey)
	if imageHistoryDays == "" {
		return 3
	}

	imageHistoryDaysInt, err := strconv.Atoi(imageHistoryDays)
	if err != nil {
		return 3
	}

	return imageHistoryDaysInt
}
